import { createApp, createVNode, h } from "vue";
import { Popup } from "vant";
import mitt from "mitt";
import type { App, ComponentPublicInstance, DefineSetupFnComponent } from "vue";
import type { PopupProps } from "vant";

export type RootPropsType = Omit<
  Partial<PopupProps & { style: CSSStyleDeclaration }>,
  "show"
> | null;

/**
 * 弹窗基类: 真正干活的类
 * 请注意，由于这里是使用了 createApp 创建了另一个 vue 实例，所有我们应用中全局注册的指令是无效的(所有的注册，都是在某个实例下的)
 * 因为这是两个实例
 * 解决方案是：除非这里传入一个实例，或者是这里注册一个一摸一样的实例；
 * 复制指令或者多个实例共享配置
 * @example
 *  import TextV from "~/components/TextV.vue";
 *
 *  export class PayDialog extends DialogFragment {
 *   protected override onCreateView() {
 *     return h(
 *       TextV,
 *       {
 *         onClickMe: () => {
 *           this.hide();
 *         },
 *       },
 *       {
 *         default: () => "slot 内容",
 *       },
 *     );
 *   }
 *
 *   protected override initWindow(): RootPropsType {
 *     return {
 *       // position: "top",
 *       round: true,
 *       style: {
 *         background: "rgba(0,0,0,0)",
 *       },
 *       closeOnClickOverlay: false,
 *     };
 *   }
 * }
 */
export abstract class DialogFragment {
  private app: App<Element> | undefined;
  private mountNode: HTMLDivElement | undefined;
  private vm: ComponentPublicInstance | undefined;
  private eventBus = mitt();
  private containerProps: { [k in string]: any } = {};
  // 给弹窗传的参数
  private rootProps?: RootPropsType = {};
  // 还需要一个执行上下文 context -> 这里就应该是另一个 vue 的 app 实例
  private context: App | undefined;

  /**
   * 这个有空需要复写，因为 popup 组件是懒加载的, 我们需要一个可以自主控制是否懒加载的组件
   */
  private popUpComponent = createPopupComponent();

  constructor() {
    this.initProps();
  }

  private initProps() {
    this.containerProps = {
      // --- popup 事件 start ---
      // 打开弹出层时立即触发
      onOpen: this.onOpen.bind(this),
      // 打开弹出层且动画结束后触发
      onOpened: this.onOpened.bind(this),
      // 关闭弹出层时立即触发
      onClose: this.onClose.bind(this),
      // 关闭弹出层且动画结束后触发
      onClosed: this.onClosed.bind(this),
      // --- popup 事件 end ---

      // --- vue 生命周期  start ---
      onBeforeCreate: this.onBeforeCreate.bind(this),
      onCreated: this.onCreated.bind(this),
      onBeforeMount: this.onBeforeMount.bind(this),
      onMounted: this.onMounted.bind(this),
      onBeforeUpdate: this.onBeforeUpdate.bind(this),
      onUpdated: this.onUpdated.bind(this),
      onBeforeUnmount: this.onBeforeUnmount.bind(this),
      onUnmounted: this.onUnmounted.bind(this),
      onErrorCaptured: this.onErrorCaptured.bind(this),
      onRenderTracked: this.onRenderTracked.bind(this),
      onRenderTriggered: this.onRenderTriggered.bind(this),
      onActivated: this.onActivated.bind(this),
      onDeactivated: this.onDeactivated.bind(this),
      onServerPrefetch: this.onServerPrefetch.bind(this),
      // --- vue 生命周期  end ---
    };

    this.rootProps = this.initWindow();
    Object.assign(this.containerProps, this.rootProps || {});
  }

  createDialog() {
    // 默认弹窗内的子元素
    const slotDefault = this.render();

    // 弹窗容器
    const modalBaseVNode = createVNode(
      this.popUpComponent.component,
      this.containerProps,
      () => h(slotDefault),
    );

    this.app = createApp(modalBaseVNode);

    // 复制上下文中的一下东西到现有弹窗中
    // 感觉可以被重写
    this.copyContextToApp();

    this.mountNode = document.createElement("div");
    this.vm = this.app.mount(this.mountNode);
    document.body.appendChild(this.mountNode);
  }

  /**
   * 设置上下文
   */
  abstract setContext(): App;

  /**
   * 把 context 中的一些属性复制到 app 中
   */
  private copyContextToApp() {
    this.context = this.setContext();

    this.copyDirectives();
  }

  /**
   * 复制上下文自定义指令
   */
  private copyDirectives() {
    if (!this.context) return;

    // 从 context 复制所有自定义指令到 app
    Object.keys(this.context._context.directives).forEach((directiveName) => {
      const directive = this.context!._context.directives[directiveName];
      this.app?.directive(directiveName, directive);
    });
  }

  /**
   * 创建视图, 使用 defineComponent 定义
   * 例子：在函数中直接返回这样的格式: defineComponent(() => () => h("div"));
   * 如何定义组件查看: https://cn.vuejs.org/api/general.html#definecomponent
   */
  abstract render(): DefineSetupFnComponent<any>;

  /**
   * 初始化容器 返回 RootPropsType 类型，其实就是 PopUp 组件的 props
   */
  protected initWindow(): RootPropsType {
    return {
      lazyRender: false, // 是否在显示弹层时才渲染节点，最好不要，因为需要做一些预加载
    };
  }

  show() {
    if (!this.vm) {
      this.createDialog();
    }
    this.popUpComponent.show();
  }

  hide() {
    this.popUpComponent.hide();
  }

  destroy() {
    // 这部的卸载只是把 div 里的内容全部清除掉
    // 并且会触发 LoadingConstructor 中的 onUnmounted 的钩子函数，所以必须执行
    // 内部其实调用的是 render(null, app._container);
    this.app?.unmount();
    if (this.mountNode) {
      document.body.removeChild(this.mountNode);
    }

    this.eventBus.all.clear();

    this.mountNode = undefined;
    this.app = undefined;
    this.vm = undefined;

    this.emit("destroyed");
  }

  on(type: string, callback: (value: any) => void) {
    // @ts-ignore
    this.eventBus.on(type, callback);
  }

  emit(type: string) {
    this.eventBus.emit(type);
  }

  protected onBeforeCreate() {
    this.emit("beforeCreate");
  }

  protected onCreated() {
    this.emit("created");
  }

  protected onBeforeMount() {
    this.emit("beforeMount");
  }

  protected onMounted() {
    this.emit("mounted");
  }

  protected onBeforeUpdate() {
    this.emit("beforeUpdate");
  }

  protected onUpdated() {
    this.emit("updated");
  }

  protected onBeforeUnmount() {
    this.emit("beforeUnmount");
  }

  protected onUnmounted() {
    this.emit("unmounted");
  }

  protected onErrorCaptured() {
    this.emit("errorCaptured");
  }

  protected onRenderTracked() {
    this.emit("renderTracked");
  }

  protected onRenderTriggered() {
    this.emit("renderTriggered");
  }

  protected onActivated() {
    this.emit("activated");
  }

  protected onDeactivated() {
    this.emit("deactivated");
  }

  protected onServerPrefetch() {
    this.emit("serverPrefetch");
  }

  protected onOpen() {
    this.emit("open");
  }

  protected onOpened() {
    this.emit("opened");
  }

  protected onClose() {
    this.emit("close");
  }

  protected onClosed() {
    this.emit("closed");
  }
}

/**
 * 创建dialog容器，这里其实可以用 expose 导出方法，但是会面临ts类型参数无法推导需要强行写一个类型
 * 类似这样，会麻烦一些
 * 这里的popup的懒加载的，visible不为true的时候是不会渲染内容的
 * type CompConstructorInstance = InstanceType<typeof CompConstructor>;
 */
function createPopupComponent() {
  const visible = ref(false);

  const show = () => {
    visible.value = true;
  };

  const hide = () => {
    visible.value = false;
  };

  /**
   * Popup 这个组件是懒加载的,在 show 之前都不会加载其内容
   */
  const PopupComponent = defineComponent({
    setup(_, { slots, emit }) {
      return () =>
        h(
          Popup,
          {
            show: visible.value,
            "onUpdate:show": (v) => (visible.value = v),
          },
          slots.default,
        );
    },
    beforeCreate() {
      this.$emit("beforeCreate");
    },
    created() {
      this.$emit("created");
    },
    beforeMount() {
      this.$emit("beforeMount");
    },
    mounted() {
      this.$emit("mounted");
    },
    beforeUpdate() {
      this.$emit("beforeUpdate");
    },
    updated() {
      this.$emit("updated");
    },
    beforeUnmount() {
      this.$emit("beforeUnmount");
    },
    unmounted() {
      this.$emit("unmounted");
    },
    errorCaptured() {
      this.$emit("errorCaptured");
    },
    renderTracked() {
      this.$emit("renderTracked");
    },
    renderTriggered() {
      this.$emit("renderTriggered");
    },
    activated() {
      this.$emit("activated");
    },
    deactivated() {
      this.$emit("deactivated");
    },
    serverPrefetch() {
      this.$emit("serverPrefetch");
    },
  });

  return { component: PopupComponent, show, hide };
}
